Vue 3 Reactivity実装してみた
Vue 3軽く紹介したかった
composition apiの説明っぽいのだけいったん貼っておく
Vue 3のReactivityシステムの簡易版を作ってみる
- 実際に作ってみることで何になるかは謎だが、とりあえずいろいろみて作ってみた
- 変数の変更 → 依存関係の更新の流れをトレース
実装してみる
まずは、特定の変数・プロパティ更新時に発火させる処理を管理しないといけない(computedの再計算・DOMの更新など)
→各依存関係はSetで管理している(重複発火を防ぐ目的もある?)
で、各々の依存関係を特定の変数・プロパティに紐付けて管理する必要もある
→これは対象の変数・プロパティをkeyにして依存関係のSetをvalueとしてMapに格納する
code: js
const depsMap = new Map() // 依存関係をkey/valueで管理するためのMap
// trackに依存先(key)と変更時の処理(effect)を渡して依存関係を追加する
const track = (key, effect) => {
const dep = depsMap.get(key) || new Set() // 対応するSetがまだなければ新規で作る
dep.add(effect)
depsMap.set(key, dep)
}
// keyを指定して、そのkeyの依存関係を全て発火させる
const trigger = (key) => {
const dep = depsMap.get(key)
if (!dep) return
dep.forEach(effect => effect())
}
実際に使ってみると
code: js
let x = 2
let total = null
const effect = () => total = x * 2
track(x, effect)
trigger(x)
console.log(x) // 2
console.log(total) // 4
x = 7
trigger(x)
console.log(x) // 7
console.log(total) // 14
って感じになる
これだとオブジェクトの場合のプロパティ変更に対応できないので更にもう一つMapを用意して、プロパティごとでtrackできる仕組みを作る
→オブジェクトをkeyにするためにWeakMapを使う
→Vueのリアクティブな値は内部的には全部Objectなので、そのための対応でもある
code: js
const targetMap = new WeakMap() // オブジェクト単位で依存関係を管理するためのMap
// trackには対象のオブジェクト(target)と対象のプロパティ(key)を渡すようになる
const track = (target, key, effect) => {
const depsMap = targetMap.get(target) || new Map()
const dep = depsMap.get(key) || new Set()
dep.add(effect)
depsMap.set(key, dep)
targetMap.set(target, depsMap)
}
// triggerも対象オブジェクト(target)を渡すようにする
const trigger = (target, key) => {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
dep.forEach(effect => effect())
}
使い方は割愛
流石にこのままだと使い勝手が悪いのでeffectの発火や追加をもっとシームレスにしたい
→reactive関数を定義する
→ES6でjsに追加されたProxyとReflectを利用する
ProxyとReflectの説明入れとく?
code: js
// 登録する関数をここに入れてからreactiveな値をgetする(ここもうちょっと良い実装考えたい。せめてクロージャにした方が良い?)
let activeEffect = null
const effect = (_effect) => {
activeEffect = _effect
activeEffect()
activeEffect = null
}
const reactive = target =>
// Proxyのハンドラを定義しておく
const handlers = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
// 値取得時にactiveEffectがある場合のみそれを登録する
if (activeEffect) {
track(target, key, activeEffect)
}
return result
},
set(target, key, value, receiver) {
const oldValue = targetkey const result = Reflect.set(target, key, value, receiver)
// 値に変化があればdepsを発火させる
if (result && oldValue !== value) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handlers)
}
使ってみた
code: js
const x = reactive({
a: 1,
b: 'hoge',
})
let twice = null
effect(() => twice = x.a * 2)
console.log(x.a) // 1
console.log(twice) // 2
x.a = 7
console.log(x.a) // 7
console.log(twice) // 14
refとcomputedも実装してみる
ref
code: js
const ref = (value) => {
const r = {
get value() {
if (activeEffect) {
track(r, 'value', activeEffect)
}
return value
},
set value(newVal) {
value = newVal
trigger(r, 'value')
}
}
return r
}
(reactiveをラップすれば簡潔に同じ挙動を実現できるが、Vue 3ではアクセサーを使っているらしい)
computed
code: js
const computed = (getter) => {
const result = ref()
effect(() => result.value = getter())
return result
}
実際refをラップしてるのかどうかは謎だったけどとりあえずこれでcomputedも実現できる
まとめ
実際はレンダリング周りとかもうちょっと複雑(依存してる値が更新されたらフラグ立てといて後でまとめて再レンダリングするみたいな挙動でした)なので、本当は実際のソースコード追いながら説明できるとよかったですね><
実際に書いてみたコード全容(書いたのとはちょっと違う)
code: js
const targetMap = new WeakMap()
const track = (target, key, effect) => {
const depsMap = targetMap.get(target) || new Map()
const dep = depsMap.get(key) || new Set()
dep.add(effect)
depsMap.set(key, dep)
targetMap.set(target, depsMap)
}
const trigger = (target, key) => {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
dep.forEach(effect => effect())
}
let activeEffect = null
const effect = (_effect) => {
activeEffect = _effect
activeEffect()
activeEffect = null
}
const reactive = target => {
const handlers = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
if (activeEffect) {
track(target, key, activeEffect)
}
return result
},
set(target, key, value, receiver) {
const oldValue = targetkey const result = Reflect.set(target, key, value, receiver)
if (result && oldValue !== value) {
trigger(target, key)
}
return result
}
}
return new Proxy(target, handlers)
}
const ref = (value) => {
const r = {
get value() {
if (activeEffect) {
track(r, 'value', activeEffect)
}
return value
},
set value(newVal) {
value = newVal
trigger(r, 'value')
}
}
return r
}
const computed = (_effect) => {
let value = null
const r = {
get value() {
if (activeEffect) {
track(r, 'value', activeEffect)
}
return value
}
}
const effect = () => {
value = _effect()
trigger(r, 'value')
}
activeEffect = effect
effect()
activeEffect = null
return r
}